package auth

import (
	"context"
	"net/http"
	"time"

	"mall/common/responsex"
	"mall/service/wpgql/api/internal/graph/model"
	"mall/service/wpgql/api/internal/graph/storage"

	"github.com/patrickmn/go-cache"
)

const cacheSeparator = "|-._.-|"

type ctxKey string

// A private key for context that only this package can access. This is important
// to prevent collisions between different context uses
var forumUserCtxKey = ctxKey("forum_user")

type AuthMiddleware struct {
	storage storage.Storage
}

func NewAuthMiddleware(db storage.Storage) *AuthMiddleware {
	return &AuthMiddleware{storage: db}
}

// Middleware decodes the share session cookie and packs the session into context
func (m *AuthMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
	// We use a cache to prevent bcrypt using too much CPU
	// also this is 60ms faster if the user is cached ;)
	// Create a cache with a default expiration time of 5 minutes, and which
	// purges expired items every 10 minutes
	c := cache.New(5*time.Minute, 10*time.Minute)

	return func(w http.ResponseWriter, r *http.Request) {
		userLogin, password, ok := r.BasicAuth()
		if !ok {
			// not all routes require authentication
			// see c.Directives.IsAuthenticated in the server.go file
			next.ServeHTTP(w, r)
			return
		}

		// let's check our cache for this user
		cacheKey := userLogin + cacheSeparator + password

		// get the user from cache
		cachedUser, userIsCached := c.Get(cacheKey)
		if userIsCached {
			// put it in context
			ctx := context.WithValue(r.Context(), forumUserCtxKey, cachedUser)

			// and call the next with our new context
			next.ServeHTTP(w, r.WithContext(ctx))
			return
		}

		// get user from storage
		user, err := m.storage.RetrieveUserByCredentials(r.Context(), userLogin, password)
		if err != nil {
			responsex.WriteJSONError(w, "Not authorized", "NOT_AUTHORIZED", http.StatusUnauthorized)
			return
		}

		// check if password is right
		// if err = bcrypt.CompareHashAndPassword(user.Password, []byte(password)); err != nil {
		// 	responsex.WriteJSONError(w, "Not authorized", "NOT_AUTHORIZED", http.StatusUnauthorized)
		// 	return
		// }

		// put the user in cache so less cpu is used for checking the hash
		c.Set(cacheKey, user, cache.DefaultExpiration)

		// put the user in context so it can be used in resolvers
		ctx := context.WithValue(r.Context(), forumUserCtxKey, user)

		// and call the next with our new context
		next.ServeHTTP(w, r.WithContext(ctx))
	}
}

// FromContext finds the user from the context. REQUIRES Middleware to have run.
func FromContextWithCheck(ctx context.Context) (*model.ForumUser, bool) {
	user, exist := ctx.Value(forumUserCtxKey).(*model.ForumUser)
	return user, exist
}

func ExistsInContext(ctx context.Context) bool {
	_, exist := ctx.Value(forumUserCtxKey).(*model.ForumUser)
	return exist
}

// FromContext finds the user from the context. REQUIRES Middleware to have run.
func FromContext(ctx context.Context) *model.ForumUser {
	user, exist := ctx.Value(forumUserCtxKey).(*model.ForumUser)
	if !exist {
		return nil
	}

	return user
}

func ForumUserIDFromContext(ctx context.Context) int {
	user := FromContext(ctx)
	if user != nil {
		return user.DatabaseID
	}

	return 0
}
